Skip to content

feat(ogar-oidc): provider-agnostic OIDC ID-token verifier (PR-01) - #367

Merged
AdaWorldAPI merged 3 commits into
mainfrom
feat/ogar-oidc-adapter
Nov 4, 2026
Merged

AdaWorldAPI merged 3 commits into
mainfrom
feat/ogar-oidc-adapter

Conversation

@AdaWorldAPI

@AdaWorldAPI AdaWorldAPI commented Nov 4, 2026 •

Copy link
Copy Markdown
Owner

What

PR-01 adds the crate ogar-oidc. It implements the AssertionVerifier interface from PR-00 (#366) for OIDC ID tokens in compact JWS form. ogar_auth::federation::establish then turns the claims it returns into a VerifiedLogin.

A provider is configured as a Client: the issuer, the client id, and the issuer's JWKS. No provider has its own code path, so ZITADEL, Keycloak and Entra ID use the same verifier.

  • No network I/O. The caller fetches the discovery document and the JWKS and passes the bytes in (Discovery::from_json, KeySet::from_jwks_json). Key rotation is Client::set_keys. The current time and the expected nonce arrive with the token (IdToken), so verification is deterministic.
  • Algorithms: ES256 and EdDSA. none, HS* and any header with crit are refused.
  • Keys.
    • A JWKS that also holds RSA keys still loads; those keys are skipped and counted.
    • Keys whose use is not sig, or whose key_ops does not list verify, are skipped the same way.
    • Keys are per issuer. The token's iss is read before the signature is checked, but only to choose whose keys to try. A key registered for one issuer can never verify a token that names another.
  • Claim checks:
    • iss must byte-equal a registered issuer and be in TrustedIssuers.
    • sub must be non-empty.
    • aud must contain the client id. With several audiences, azp is required, and when present it must equal the client id.
    • exp and iat are required and nbf is optional, all checked with a leeway (default 60 s).
    • nonce must match when the login sent one.
  • Strength. MultiFactor requires either mfa in amr, or amr values covering two independent factor categories: knowledge, possession, inherence (RFC 8176). user (presence) is not a factor. Anything else gives SingleFactor.
  • Email. It is returned only when email_verified is true, and only as a display hint.
  • Errors. OidcError carries no token contents.

Tests

The 16 integration tests are in crates/ogar-oidc/tests/verifier.rs. They include an end-to-end establish run with both a bound and an unbound identity.

Disable runs. Each one turns red only the test named next to it:

change made red test
search every issuer for the key key_of_one_issuer_never_verifies_another
unknown alg defaults to ES256 alg_none_hs256_and_crit_are_unsupported
crit accepted alg_none_hs256_and_crit_are_unsupported
no azp rule for several audiences audience_and_authorized_party_rules
no TrustedIssuers check issuer_is_byte_exact_and_must_be_trusted
exp leeway off by one time_claims_and_leeway_boundaries
nonce ignored nonce_must_match_when_expected
email used without email_verified email_is_returned_only_when_verified
key type not matched to alg kid_selection_rules
any two amr labels count as MFA amr_decides_strength
key_ops ignored jwks_parsing_skips_and_rejects

Gates:

  • cargo test -p ogar-oidc -p ogar-auth: green.
  • cargo clippy -p ogar-oidc --all-targets -- -D warnings: clean.
  • cargo fmt --all -- --check: clean.

Dependencies (forks first)

  • p256: AdaWorldAPI/elliptic-curves at rev 739304e026fdf06cd1a31606e4db487d3f47c5ae, the same rev ndarray's encryption crate pins.
  • ecdsa 0.17.0: redirected by a new workspace [patch.crates-io] to AdaWorldAPI/signatures at rev 5595c3b1bfa14022b46ec3cc662a87e49b954266. cargo tree -i ecdsa confirms the patch is used.
  • Ed25519: through ogar-encryption.
  • base64 and serde_json: crates.io.
  • The new dependency row is recorded in docs/UPSTREAM-DEPS.md.

Decision: no RS256

The operator ruled to keep the verifier ES256/EdDSA only. A provider must sign ID tokens with a P-256 or Ed25519 key. ZITADEL's default signing key is RSA, so a ZITADEL instance needs an ECDSA or Ed25519 signing key activated.

Correction to PR-00's description

The #366 description said Issuer accepts http for localhost. It does not: Issuer::new accepts https:// only, which is what docs/AUTH-EXTERNAL-IDENTITY.md §1 states.

Not in scope

  • Fetching JWKS or discovery over HTTP, and caching them.
  • The report-rs login routes (authorization code + PKCE).
  • Role policies.
  • Wiring the remaining RustCrypto forks: that is the next PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XAqKMQJzQD5KvNm8BtbsWG

New crate ogar-oidc implementing ogar_auth::federation::AssertionVerifier for
compact JWS ID tokens:

- ES256 (p256 from the AdaWorldAPI/elliptic-curves fork; ecdsa patched to the
  AdaWorldAPI/signatures fork) and EdDSA (ogar-encryption). none, HS* and crit
  are refused; RS256 is not implemented (no RSA fork wired).
- Keys are looked up only in the token issuer's own KeySet, so one issuer's key
  never verifies a token naming another.
- iss byte-equal and trusted, sub, aud/azp, exp/iat/nbf with leeway, nonce;
  strength from amr; email only when email_verified.
- No network I/O: Discovery::from_json and KeySet::from_jwks_json take bytes
  the caller fetched; the clock and nonce come in with the token.

16 integration tests; nine disable runs (key search across issuers, unknown
alg defaulting to ES256, no azp rule, no trust check, exp off by one, nonce
ignored, unverified email, key type not matched to alg, crit accepted) each
turn their test red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAqKMQJzQD5KvNm8BtbsWG
@coderabbitai

coderabbitai Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Important

  • Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 4e0b3b0b-ed20-45c0-a17e-c4d04b082377
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-04T22:47:42.152293Z 539a7db Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6e50aae0eb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/ogar-oidc/src/lib.rs Outdated
Comment on lines +440 to +442
let distinct = FACTORS.iter().filter(|f| values.contains(f)).count();
if distinct >= 2 {
AuthStrength::MultiFactor

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Count independent factors rather than distinct AMR labels

A valid token with amr: ["hwk", "user"] is classified as MultiFactor, although it can describe a single hardware-key authentication with a user-presence check. RFC 8176 §2 defines user as evidence of presence, not an independent authentication factor. Similarly, different method labels can describe the same factor. establish copies this result directly into the authenticated user's context, so these logins receive an overstated authentication strength. Infer MFA only from combinations that establish independent factors, or conservatively require an explicit trusted MFA assertion; add a regression test for hardware-key authentication with presence only.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 38c7b37.

MultiFactor now requires either mfa in amr, or amr values that cover two independent factor categories (RFC 8176): knowledge (pwd pin kba), possession (hwk swk otp sms tel pop sc) or inherence (fpt face iris retina vbm). user (presence) and other labels that are not a factor count for nothing.

New cases in amr_decides_strength:

  • ["hwk","user"] → SingleFactor;
  • ["otp","sms"] (one category) → SingleFactor;
  • ["hwk","fpt"] → MultiFactor.

Restoring the old rule, where any two labels counted, turns the test red.


Generated by Claude Code

amr ["hwk","user"] described one hardware-key authentication with a
presence check, yet counted as two factors. Strength now requires mfa or
two of knowledge / possession / inherence; user (presence) is not a factor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAqKMQJzQD5KvNm8BtbsWG

Copy link
Copy Markdown
Owner Author

@codex review


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 38c7b37246

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +139 to +142
let Some(key) = parse_jwk(jwk)? else {
set.skipped += 1;
continue;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor JWK operation restrictions when importing verification keys

When a JWKS expresses key restrictions through key_ops rather than use, this loader ignores them. For example, a P-256 key with key_ops: ["deriveKey"] and no use or alg is imported as an ES256 verification key, and tokens signed with its private key can pass verify even though the issuer did not authorize that key for signature verification. Check key_ops when present and exclude keys whose permitted operations do not include verify; add a regression test alongside the existing encryption-key filtering test.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 539a7db.

KeySet::from_jwks_json now skips any key whose key_ops is present but does not list verify, and also any key whose key_ops is not an array (RFC 7517 §4.3). It counts each skipped key in skipped().

Two cases were added to jwks_parsing_skips_and_rejects:

  • a P-256 key with key_ops: ["deriveKey"] and no use is skipped;
  • a key with key_ops: ["verify"] is kept.

The test goes red when the check is removed.


Generated by Claude Code

…Codex P2)

A key whose key_ops is present but lacks "verify" (or is not an array) is
skipped, per RFC 7517 §4.3, instead of being imported as a verification key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAqKMQJzQD5KvNm8BtbsWG

Copy link
Copy Markdown
Owner Author

@codex review


Generated by Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: 539a7dbec7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@AdaWorldAPI
AdaWorldAPI merged commit b0edfc3 into main Nov 4, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants